import com.orientechnologies.orient.core.metadata.schema.OType
import com.orientechnologies.orient.core.record.impl.ODocument
import org.slf4j.Logger
import org.sonatype.nexus.repository.Repository
import org.sonatype.nexus.repository.storage.Query
import org.sonatype.nexus.repository.storage.StorageFacet
import org.sonatype.nexus.repository.storage.StorageTx
import org.sonatype.nexus.script.plugin.internal.provisioning.RepositoryApiImpl

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

class DockerRepositoryCleanUpWorker {

    private final Logger log
    private final RepositoryApiImpl repositoryApi

    private final def selfDescription = 'Clean Up Unused Images'

    // 说明: docker的registry对应nexus的repository,
    // 说明: docker registry中的repository对应nexus repository中的目录(路径)
    // 说明: docker repository中的image对应nexus 目录下的component
    // 指定要清理的docker registry
    private final def targetRepositoryName = 'niceloo-k8s'

    // 指定namespace, 只清理指定namespace中的docker repository
    private final def targetImageNamePrefix = 'youlu/'

    // docker repository中的镜像是否根据版本后缀区分环境, 即不同环境的镜像使用不同的版本后缀进行区分
    private final String[] usedImageVersionSuffixes = ['.dev', '.test']

    // 上传到docker repository的镜像至少n天才会过期, 过期之前肯定不会被删除, 是否过期取决于镜像的last updated日期
    private final def daysAtLeastKeep = 0

    // 镜像过期之后才会被删除, 但是会保留至少n个最近过期的镜像
    private final def imagesAtLeastKeepAfterExpirationDate = 3

    // 指定需要忽略的镜像版本, 该版本的镜像肯定不会被删除
    private final def ignoreImageVersions = []

    // 指定关键字, 镜像名称包含指定的关键字时, 该镜像的所有版本都不会被删除
    private final def ignoreImageNameKeywords = ['agent-server', 'openjdk', 'baseimage']

    DockerRepositoryCleanUpWorker(log, repository) {
        this.log = log
        this.repositoryApi = repository
    }

    void cleanUp() {
        log.info "$selfDescription:::started!"

        Repository targetRepository = repositoryApi.repositoryManager.get(targetRepositoryName)

        check(targetRepository)

        StorageTx storageTx = targetRepository.facet(StorageFacet).txSupplier().get()
        log.info "initiated storageTx instance: $storageTx"

        try {
            storageTx.begin()

            log.info 'asset count before clean up: {}', storageTx.countAssets(Query.builder().build(), [targetRepository])
            log.info 'component count before clean up: {}', storageTx.countComponents(Query.builder().build(), [targetRepository])

            def selectDockerRepositoryNameSql = ' SELECT distinct(name) AS name FROM component WHERE name LIKE :namePrefix AND bucket= :bucketId '
            def params = [
                    namePrefix: "$targetImageNamePrefix%",
                    bucketId: storageTx.findBucket(targetRepository).entityMetadata.id.identity.toString()
            ]

            storageTx.browse(selectDockerRepositoryNameSql, params).each { oDocument ->
                String dockerRepositoryName = (oDocument as ODocument).field('name', OType.STRING)

                if (ignoreImageNameKeywords) {
                    ignoreImageNameKeywords.each {
                        if (dockerRepositoryName.contains(it)) {
                            log.info "ignore $dockerRepositoryName, no clean up"
                            return
                        }
                    }
                }

                log.info "start clean up $dockerRepositoryName"

                if (usedImageVersionSuffixes) {
                    usedImageVersionSuffixes.each {
                        doCleanUpDockerRepository(storageTx, targetRepository, dockerRepositoryName, it)
                    }
                } else {
                    doCleanUpDockerRepository(storageTx, targetRepository, dockerRepositoryName, '')
                }
            }

            log.info 'asset count after clean up: {}', storageTx.countAssets(Query.builder().build(), [targetRepository])
            log.info 'component count after clean up: {}', storageTx.countComponents(Query.builder().build(), [targetRepository])

            storageTx.commit()
        } catch (Throwable t) {
            log.error "transaction failed $t"
            storageTx.rollback()
        } finally {
            storageTx.close()
        }
    }

    private void doCleanUpDockerRepository(StorageTx storageTx, Repository repository, String dockerRepositoryName, String versionSuffix) {
        def where = ' name = :name AND last_updated < :lastUpdated '
        def params = [
                name: dockerRepositoryName,
                lastUpdated: LocalDateTime.now().minusDays(daysAtLeastKeep).format(DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss'))
        ]

        if (versionSuffix) {
            where += " AND version LIKE '%$versionSuffix' "
        }

        if (ignoreImageVersions) {
            ignoreImageVersions.each {
                where += " AND version <> '$it' "
            }
        }

        def count = storageTx.countComponents(where, params, [repository], '')
        if (count <= imagesAtLeastKeepAfterExpirationDate) {
            log.info "$dockerRepositoryName only has $count expired $versionSuffix versions, no cleanup is necessary"
            return
        }

        def querySuffix = " ORDER BY last_updated DESC SKIP $imagesAtLeastKeepAfterExpirationDate "
        storageTx.findComponents(where, params, [repository], querySuffix).each {
            log.info "$it will be deleted"
            storageTx.deleteComponent(it)
        }
    }

    private void check(Repository repository) {
        if (!repository) {
            log.error "$targetRepositoryName does not exist!!!"
            log.error "$selfDescription:::aborted!!!"
            return
        }

        log.info "$selfDescription:::target repository:::$repository"

        def targetRepositoryFormat = 'docker'
        if (repository.format.value != targetRepositoryFormat) {
            log.error "$targetRepositoryName format is not $targetRepositoryFormat!!!"
            log.error "$selfDescription:::aborted!!!"
            return
        }

        def targetRepositoryType = 'hosted'
        if (repository.type.value != targetRepositoryType) {
            log.error "$targetRepositoryName type is not$targetRepositoryType!!!"
            log.error "$selfDescription:::aborted!!!"
        }
    }

}

new DockerRepositoryCleanUpWorker(log, repository).cleanUp()
